iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
AI Engineering

30 天打造我的 AI 開發工作流:從需求分析到上線系列 第 20

Day 20|資料庫與 Alembic:migration 的人工 review

  • 分享至 

  • xImage
  •  

前言

migration 跟測試走的是兩條不同的路,所以「tests 全綠」不能證明 migration 是安全的。


昨天說到

昨天講的是架構規則怎麼被檢查,今天要講 Migration 比較麻煩。它是整個專案裡幾乎沒有任何既有驗證流程會直接檢查的產出:

誰會看 會看 migration 嗎
測試 ❌ 測試走 create_all 從 metadata 建表,永遠不經過 alembic
typecheck ❌ 它是 SQL,不是型別
code review ⚠️ 它長得像機器產的,通常被捲過去
使用者 ❌ 錯了要到部署那天才知道

而它偏偏又是唯一一個會直接改動資料庫 schema 的產出,所以今天我決定故意拿一支 migration 做實驗。


測試

這個專案目前的 schema 和程式碼理論上是同步的。

所以我直接對目前的狀態跑一次,照理說應該產出一支空 migration。

$ uv run alembic revision --autogenerate -m "autogenerate probe"

INFO  [alembic.autogenerate.compare.check_constraints]
      Detected removed check constraint 'version_status' on table 'note_versions'
INFO  [alembic.autogenerate.compare.check_constraints]
      Detected removed check constraint 'decision_action' on table 'version_decisions'

結果不是空的。

Alembic 偵測到兩個 CHECK constraint 被移除,產出的 migration 是:

def upgrade() -> None:
    op.drop_constraint(op.f('version_status'), 'note_versions', type_='check')
    op.drop_constraint(op.f('decision_action'), 'version_decisions', type_='check')

先確認這兩個 constraint 是不是真的漏掉了:

① 資料庫裡真的有嗎: ['version_status', 'decision_action']
② metadata 裡有嗎:  ['note_versions.version_status', 'version_decisions.decision_action']

這是假陽性。

原因最後追到 native_enum=False 產生的 CHECK constraint 在比對時無法正確對應。但這裡真正值得記住的不是原因,而是:

autogenerate 不只是「可能漏掉東西」,也可能把它看到的東西判斷錯。


套用

接著我把這支 migration 放到乾淨資料庫實際跑一次。

結果:

原本的 schema
→ status 非法值:擋住

套用 autogenerate migration
→ status 非法值:可以寫進去

也就是那個 CHECK constraint 真的被拿掉了,但同一時間,原本的 45 條測試仍然全部通過。

測試
→ create_all()
→ Base.metadata
→ 建立 schema

正式部署
→ alembic upgrade head
→ 執行 migration
→ 建立 schema

兩條路根本不一樣。

一支 migration 可以拆掉一條業務規則,而整套測試完全不知道。


autogenerate 的盲區

autogenerate 比對的是 Base.metadata 與資料庫,不是在理解整個資料庫設計。
所以有些東西它根本看不到,有些東西則可能比對錯。

目前採到的坑:

盲區 情況
trigger / function 完全看不見——不會產生,也不會發現它不見了
WHERE 的 partial index 偵測不可靠,plan.md 早就記著「產完必須人工核對」
沒 import 進 models/__init__.py 的 model 看不見(env.py 用的是 Base.metadata
native_enum=False 產生的 CHECK 比對不出來,會誤判為「被移除」
資料(seed) 它只看 schema

人工 review

這是我調整最多的地方。原本我看 migration 的方式是讀那段 SQL、確認它符合我要的改動——那是在檢查「它產得對不對」。

但這個實驗裡的那支 migration,SQL 完全正確:它確實會 drop 掉那兩個約束,語法沒問題,downgrade 也寫好了。它產得很對,只是它想做的事是錯的。

所以問題要換成:它有沒有看見? 產完之後,逐項對一次盲區清單——這份 migration 該碰的東西裡,有沒有哪一類是它看不見或會猜錯的?


產完 migration,要檢查三件事

檢查 這個專案抓到什麼
1 它有沒有看見? 對照盲區清單逐項確認 它想 drop 掉兩個正在運作的約束
2 downgrade 真的跑得動嗎?
3 在一個空資料庫上,從頭跑到 head 會成功嗎? VARCHAR(32) 那個

第 3 個尤其重要。

因為開發資料庫通常都是一路升上來的,很少真的從:

base
↓
001
↓
002
↓
003
↓
head

完整跑一次。

但正式環境第一次部署,走的正是這條路。

目前這三件事還是手動的。

下一步才是把它放進 CI,讓 CI 自動建立空資料庫,執行 upgrade head,再驗證整套測試。


結論

  • 沒有人在看的產出,錯誤會越積越多。
  • 產得對不對」和「它有沒有看見」是兩個問題:autogenerate 產出的 SQL 可以完全正確,而它想做的事完全錯誤。review 的時候問後者,不要只問前者。
  • 綠燈的範圍比它看起來的窄。

明天:資料庫的債清完了,明天要來講的是認證與權限:JWT + RBAC。


上一篇
Day 19|寫在 CLAUDE.md 裡的架構規則,擋得住什麼?
下一篇
Day 21|認證與權限:JWT、RBAC
系列文
30 天打造我的 AI 開發工作流:從需求分析到上線22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言